<!-- HTML header for doxygen 1.8.6-->
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/xhtml;charset=UTF-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=9"/>
<meta name="generator" content="Doxygen 1.8.13"/>
<title>OpenCV: Using a cv::cuda::GpuMat with thrust</title>
<link href="../../opencv.ico" rel="shortcut icon" type="image/x-icon" />
<link href="../../tabs.css" rel="stylesheet" type="text/css"/>
<script type="text/javascript" src="../../jquery.js"></script>
<script type="text/javascript" src="../../dynsections.js"></script>
<script type="text/javascript" src="../../tutorial-utils.js"></script>
<link href="../../search/search.css" rel="stylesheet" type="text/css"/>
<script type="text/javascript" src="../../search/searchdata.js"></script>
<script type="text/javascript" src="../../search/search.js"></script>
<script type="text/x-mathjax-config">
  MathJax.Hub.Config({
    extensions: ["tex2jax.js", "TeX/AMSmath.js", "TeX/AMSsymbols.js"],
    jax: ["input/TeX","output/HTML-CSS"],
});
//<![CDATA[
MathJax.Hub.Config(
{
  TeX: {
      Macros: {
          matTT: [ "\\[ \\left|\\begin{array}{ccc} #1 & #2 & #3\\\\ #4 & #5 & #6\\\\ #7 & #8 & #9 \\end{array}\\right| \\]", 9],
          fork: ["\\left\\{ \\begin{array}{l l} #1 & \\mbox{#2}\\\\ #3 & \\mbox{#4}\\\\ \\end{array} \\right.", 4],
          forkthree: ["\\left\\{ \\begin{array}{l l} #1 & \\mbox{#2}\\\\ #3 & \\mbox{#4}\\\\ #5 & \\mbox{#6}\\\\ \\end{array} \\right.", 6],
          forkfour: ["\\left\\{ \\begin{array}{l l} #1 & \\mbox{#2}\\\\ #3 & \\mbox{#4}\\\\ #5 & \\mbox{#6}\\\\ #7 & \\mbox{#8}\\\\ \\end{array} \\right.", 8],
          vecthree: ["\\begin{bmatrix} #1\\\\ #2\\\\ #3 \\end{bmatrix}", 3],
          vecthreethree: ["\\begin{bmatrix} #1 & #2 & #3\\\\ #4 & #5 & #6\\\\ #7 & #8 & #9 \\end{bmatrix}", 9],
          cameramatrix: ["#1 = \\begin{bmatrix} f_x & 0 & c_x\\\\ 0 & f_y & c_y\\\\ 0 & 0 & 1 \\end{bmatrix}", 1],
          distcoeffs: ["(k_1, k_2, p_1, p_2[, k_3[, k_4, k_5, k_6 [, s_1, s_2, s_3, s_4[, \\tau_x, \\tau_y]]]]) \\text{ of 4, 5, 8, 12 or 14 elements}"],
          distcoeffsfisheye: ["(k_1, k_2, k_3, k_4)"],
          hdotsfor: ["\\dots", 1],
          mathbbm: ["\\mathbb{#1}", 1],
          bordermatrix: ["\\matrix{#1}", 1]
      }
  }
}
);
//]]>
</script><script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.0/MathJax.js"></script>
<link href="../../doxygen.css" rel="stylesheet" type="text/css" />
<link href="../../stylesheet.css" rel="stylesheet" type="text/css"/>
</head>
<body>
<div id="top"><!-- do not remove this div, it is closed by doxygen! -->
<div id="titlearea">
<!--#include virtual="/google-search.html"-->
<table cellspacing="0" cellpadding="0">
 <tbody>
 <tr style="height: 56px;">
  <td id="projectlogo"><img alt="Logo" src="../../opencv-logo-small.png"/></td>
  <td style="padding-left: 0.5em;">
   <div id="projectname">OpenCV
   &#160;<span id="projectnumber">4.5.2</span>
   </div>
   <div id="projectbrief">Open Source Computer Vision</div>
  </td>
 </tr>
 </tbody>
</table>
</div>
<!-- end header part -->
<!-- Generated by Doxygen 1.8.13 -->
<script type="text/javascript">
var searchBox = new SearchBox("searchBox", "../../search",false,'Search');
</script>
<script type="text/javascript" src="../../menudata.js"></script>
<script type="text/javascript" src="../../menu.js"></script>
<script type="text/javascript">
$(function() {
  initMenu('../../',true,false,'search.php','Search');
  $(document).ready(function() { init_search(); });
});
</script>
<div id="main-nav"></div>
<!-- window showing the filter options -->
<div id="MSearchSelectWindow"
     onmouseover="return searchBox.OnSearchSelectShow()"
     onmouseout="return searchBox.OnSearchSelectHide()"
     onkeydown="return searchBox.OnSearchSelectKey(event)">
</div>

<!-- iframe showing the search results (closed by default) -->
<div id="MSearchResultsWindow">
<iframe src="javascript:void(0)" frameborder="0" 
        name="MSearchResults" id="MSearchResults">
</iframe>
</div>

<div id="nav-path" class="navpath">
  <ul>
<li class="navelem"><a class="el" href="../../d9/df8/tutorial_root.html">OpenCV Tutorials</a></li><li class="navelem"><a class="el" href="../../da/d2c/tutorial_table_of_content_gpu.html">GPU-Accelerated Computer Vision (cuda module)</a></li>  </ul>
</div>
</div><!-- top -->
<div class="header">
  <div class="headertitle">
<div class="title">Using a <a class="el" href="../../d0/d60/classcv_1_1cuda_1_1GpuMat.html" title="Base storage class for GPU memory with reference counting. ">cv::cuda::GpuMat</a> with thrust </div>  </div>
</div><!--header-->
<div class="contents">
<div class="textblock"><p><b>Prev Tutorial:</b> <a class="el" href="../../dd/d3d/tutorial_gpu_basics_similarity.html">Similarity check (PNSR and SSIM) on the GPU</a></p>
<h2>Goal </h2>
<p>Thrust is an extremely powerful library for various cuda accelerated algorithms. However thrust is designed to work with vectors and not pitched matricies. The following tutorial will discuss wrapping <a class="el" href="../../d0/d60/classcv_1_1cuda_1_1GpuMat.html" title="Base storage class for GPU memory with reference counting. ">cv::cuda::GpuMat</a>'s into thrust iterators that can be used with thrust algorithms.</p>
<p>This tutorial should show you how to:</p><ul>
<li>Wrap a GpuMat into a thrust iterator</li>
<li>Fill a GpuMat with random numbers</li>
<li>Sort a column of a GpuMat in place</li>
<li>Copy values greater than 0 to a new gpu matrix</li>
<li>Use streams with thrust</li>
</ul>
<h2>Wrapping a GpuMat into a thrust iterator </h2>
<p>The following code will produce an iterator for a GpuMat</p>
<div class="fragment"><div class="line"><span class="comment">/*</span></div><div class="line"><span class="comment">    @Brief GpuMatBeginItr returns a thrust compatible iterator to the beginning of a GPU mat&#39;s memory.</span></div><div class="line"><span class="comment">    @Param mat is the input matrix</span></div><div class="line"><span class="comment">    @Param channel is the channel of the matrix that the iterator is accessing.  If set to -1, the iterator will access every element in sequential order</span></div><div class="line"><span class="comment">*/</span></div><div class="line"><span class="keyword">template</span>&lt;<span class="keyword">typename</span> T&gt;</div><div class="line">thrust::permutation_iterator&lt;thrust::device_ptr&lt;T&gt;, thrust::transform_iterator&lt;step_functor&lt;T&gt;, thrust::counting_iterator&lt;int&gt;&gt;&gt;  GpuMatBeginItr(<a class="code" href="../../d0/d60/classcv_1_1cuda_1_1GpuMat.html">cv::cuda::GpuMat</a> mat, <span class="keywordtype">int</span> channel = 0)</div><div class="line">{</div><div class="line">    <span class="keywordflow">if</span> (channel == -1)</div><div class="line">    {</div><div class="line">        mat = mat.<a class="code" href="../../d0/d60/classcv_1_1cuda_1_1GpuMat.html#a408e22ed824d1ddf59f58bda895017a8">reshape</a>(1);</div><div class="line">        channel = 0;</div><div class="line">    }</div><div class="line">    <a class="code" href="../../db/de0/group__core__utils.html#gaf62bcd90f70e275191ab95136d85906b">CV_Assert</a>(mat.<a class="code" href="../../d0/d60/classcv_1_1cuda_1_1GpuMat.html#aaa229d9b2b2f60ecae3b5fbf0603c1b9">depth</a>() == <a class="code" href="../../d0/d3a/classcv_1_1DataType.html">cv::DataType&lt;T&gt;::depth</a>);</div><div class="line">    <a class="code" href="../../db/de0/group__core__utils.html#gaf62bcd90f70e275191ab95136d85906b">CV_Assert</a>(channel &lt; mat.<a class="code" href="../../d0/d60/classcv_1_1cuda_1_1GpuMat.html#a538fc6d75281b4ecb7ad50e4555f3fc6">channels</a>());</div><div class="line">    <span class="keywordflow">return</span> thrust::make_permutation_iterator(thrust::device_pointer_cast(mat.<a class="code" href="../../d0/d60/classcv_1_1cuda_1_1GpuMat.html#aa83fa0825c60eb22a11a87a98c3cd5ed">ptr</a>&lt;T&gt;(0) + channel),</div><div class="line">        thrust::make_transform_iterator(thrust::make_counting_iterator(0), step_functor&lt;T&gt;(mat.<a class="code" href="../../d0/d60/classcv_1_1cuda_1_1GpuMat.html#a9265a32d8d29fe29804a0cb8f57213e9">cols</a>, mat.<a class="code" href="../../d0/d60/classcv_1_1cuda_1_1GpuMat.html#af46427ea4c9b3fe7687e3afa84baede3">step</a> / <span class="keyword">sizeof</span>(T), mat.<a class="code" href="../../d0/d60/classcv_1_1cuda_1_1GpuMat.html#a538fc6d75281b4ecb7ad50e4555f3fc6">channels</a>())));</div><div class="line">}</div></div><!-- fragment --><div class="fragment"><div class="line"><span class="comment">/*</span></div><div class="line"><span class="comment">@Brief GpuMatEndItr returns a thrust compatible iterator to the end of a GPU mat&#39;s memory.</span></div><div class="line"><span class="comment">@Param mat is the input matrix</span></div><div class="line"><span class="comment">@Param channel is the channel of the matrix that the iterator is accessing.  If set to -1, the iterator will access every element in sequential order</span></div><div class="line"><span class="comment">*/</span></div><div class="line"><span class="keyword">template</span>&lt;<span class="keyword">typename</span> T&gt;</div><div class="line">thrust::permutation_iterator&lt;thrust::device_ptr&lt;T&gt;, thrust::transform_iterator&lt;step_functor&lt;T&gt;, thrust::counting_iterator&lt;int&gt;&gt;&gt;  GpuMatEndItr(<a class="code" href="../../d0/d60/classcv_1_1cuda_1_1GpuMat.html">cv::cuda::GpuMat</a> mat, <span class="keywordtype">int</span> channel = 0)</div><div class="line">{</div><div class="line">    <span class="keywordflow">if</span> (channel == -1)</div><div class="line">    {</div><div class="line">        mat = mat.<a class="code" href="../../d0/d60/classcv_1_1cuda_1_1GpuMat.html#a408e22ed824d1ddf59f58bda895017a8">reshape</a>(1);</div><div class="line">        channel = 0;</div><div class="line">    }</div><div class="line">    <a class="code" href="../../db/de0/group__core__utils.html#gaf62bcd90f70e275191ab95136d85906b">CV_Assert</a>(mat.<a class="code" href="../../d0/d60/classcv_1_1cuda_1_1GpuMat.html#aaa229d9b2b2f60ecae3b5fbf0603c1b9">depth</a>() == <a class="code" href="../../d0/d3a/classcv_1_1DataType.html">cv::DataType&lt;T&gt;::depth</a>);</div><div class="line">    <a class="code" href="../../db/de0/group__core__utils.html#gaf62bcd90f70e275191ab95136d85906b">CV_Assert</a>(channel &lt; mat.<a class="code" href="../../d0/d60/classcv_1_1cuda_1_1GpuMat.html#a538fc6d75281b4ecb7ad50e4555f3fc6">channels</a>());</div><div class="line">    <span class="keywordflow">return</span> thrust::make_permutation_iterator(thrust::device_pointer_cast(mat.<a class="code" href="../../d0/d60/classcv_1_1cuda_1_1GpuMat.html#aa83fa0825c60eb22a11a87a98c3cd5ed">ptr</a>&lt;T&gt;(0) + channel),</div><div class="line">        thrust::make_transform_iterator(thrust::make_counting_iterator(mat.<a class="code" href="../../d0/d60/classcv_1_1cuda_1_1GpuMat.html#a7385022ca9114e5f5058dbb2f12467cb">rows</a>*mat.<a class="code" href="../../d0/d60/classcv_1_1cuda_1_1GpuMat.html#a9265a32d8d29fe29804a0cb8f57213e9">cols</a>), step_functor&lt;T&gt;(mat.<a class="code" href="../../d0/d60/classcv_1_1cuda_1_1GpuMat.html#a9265a32d8d29fe29804a0cb8f57213e9">cols</a>, mat.<a class="code" href="../../d0/d60/classcv_1_1cuda_1_1GpuMat.html#af46427ea4c9b3fe7687e3afa84baede3">step</a> / <span class="keyword">sizeof</span>(T), mat.<a class="code" href="../../d0/d60/classcv_1_1cuda_1_1GpuMat.html#a538fc6d75281b4ecb7ad50e4555f3fc6">channels</a>())));</div><div class="line">}</div></div><!-- fragment --><p> Our goal is to have an iterator that will start at the beginning of the matrix, and increment correctly to access continuous matrix elements. This is trivial for a continuous row, but how about for a column of a pitched matrix? To do this we need the iterator to be aware of the matrix dimensions and step. This information is embedded in the step_functor. </p><div class="fragment"><div class="line"><span class="keyword">template</span>&lt;<span class="keyword">typename</span> T&gt; <span class="keyword">struct </span>step_functor : <span class="keyword">public</span> thrust::unary_function&lt;int, int&gt;</div><div class="line">{</div><div class="line">    <span class="keywordtype">int</span> columns;</div><div class="line">    <span class="keywordtype">int</span> step;</div><div class="line">    <span class="keywordtype">int</span> channels;</div><div class="line">    __host__ __device__ step_functor(<span class="keywordtype">int</span> columns_, <span class="keywordtype">int</span> step_, <span class="keywordtype">int</span> channels_ = 1) : columns(columns_), step(step_), channels(channels_)  {   };</div><div class="line">    __host__ step_functor(<a class="code" href="../../d0/d60/classcv_1_1cuda_1_1GpuMat.html">cv::cuda::GpuMat</a>&amp; mat)</div><div class="line">    {</div><div class="line">        <a class="code" href="../../db/de0/group__core__utils.html#gaf62bcd90f70e275191ab95136d85906b">CV_Assert</a>(mat.<a class="code" href="../../d0/d60/classcv_1_1cuda_1_1GpuMat.html#aaa229d9b2b2f60ecae3b5fbf0603c1b9">depth</a>() == <a class="code" href="../../d0/d3a/classcv_1_1DataType.html">cv::DataType&lt;T&gt;::depth</a>);</div><div class="line">        columns = mat.<a class="code" href="../../d0/d60/classcv_1_1cuda_1_1GpuMat.html#a9265a32d8d29fe29804a0cb8f57213e9">cols</a>;</div><div class="line">        step = mat.<a class="code" href="../../d0/d60/classcv_1_1cuda_1_1GpuMat.html#af46427ea4c9b3fe7687e3afa84baede3">step</a> / <span class="keyword">sizeof</span>(T);</div><div class="line">        channels = mat.<a class="code" href="../../d0/d60/classcv_1_1cuda_1_1GpuMat.html#a538fc6d75281b4ecb7ad50e4555f3fc6">channels</a>();</div><div class="line">    }</div><div class="line">    __host__ __device__</div><div class="line">        <span class="keywordtype">int</span> operator()(<span class="keywordtype">int</span> x)<span class="keyword"> const</span></div><div class="line"><span class="keyword">    </span>{</div><div class="line">        <span class="keywordtype">int</span> row = x / columns;</div><div class="line">        <span class="keywordtype">int</span> idx = (row * step) + (x % columns)*channels;</div><div class="line">        <span class="keywordflow">return</span> idx;</div><div class="line">    }</div><div class="line">};</div></div><!-- fragment --><p>The step functor takes in an index value and returns the appropriate offset from the beginning of the matrix. The counting iterator simply increments over the range of pixel elements. Combined into the transform_iterator we have an iterator that counts from 0 to M*N and correctly increments to account for the pitched memory of a GpuMat. Unfortunately this does not include any memory location information, for that we need a thrust::device_ptr. By combining a device pointer with the transform_iterator we can point thrust to the first element of our matrix and have it step accordingly.</p>
<h2>Fill a GpuMat with random numbers </h2>
<p>Now that we have some nice functions for making iterators for thrust, lets use them to do some things OpenCV can't do. Unfortunately at the time of this writing, OpenCV doesn't have any Gpu random number generation. Thankfully thrust does and it's now trivial to interop between the two. Example taken from <a href="http://stackoverflow.com/questions/12614164/generating-a-random-number-vector-between-0-and-1-0-using-thrust">http://stackoverflow.com/questions/12614164/generating-a-random-number-vector-between-0-and-1-0-using-thrust</a></p>
<p>First we need to write a functor that will produce our random values. </p><div class="fragment"><div class="line">struct prg</div><div class="line">{</div><div class="line">  float a, b;</div><div class="line"></div><div class="line">  __host__ __device__</div><div class="line">    prg(float _a = 0.f, float _b = 1.f) : a(_a), b(_b) {};</div><div class="line"></div><div class="line">  __host__ __device__</div><div class="line">    float operator()(const unsigned int n) const</div><div class="line">  {</div><div class="line">    thrust::default_random_engine rng;</div><div class="line">    thrust::uniform_real_distribution&lt;float&gt; dist(a, b);</div><div class="line">    rng.discard(n);</div><div class="line">    return dist(rng);</div><div class="line">  }</div><div class="line">};</div></div><!-- fragment --><p> This will take in an integer value and output a value between a and b. Now we will populate our matrix with values between 0 and 10 with a thrust transform. </p><div class="fragment"><div class="line">  {</div><div class="line">    cv::cuda::GpuMat d_value(1, 100, CV_32F);</div><div class="line">    auto valueBegin = GpuMatBeginItr&lt;float&gt;(d_value);</div><div class="line">    auto valueEnd = GpuMatEndItr&lt;float&gt;(d_value);</div><div class="line">    thrust::transform(thrust::make_counting_iterator(0), thrust::make_counting_iterator(d_value.cols), valueBegin, prg(-1, 1));</div><div class="line"></div><div class="line">    cv::Mat h_value(d_value);</div><div class="line">  }</div></div><!-- fragment --> <h2>Sort a column of a GpuMat in place </h2>
<p>Lets fill matrix elements with random values and an index. Afterwards we will sort the random numbers and the indecies. </p><div class="fragment"><div class="line">  {</div><div class="line">    cv::cuda::GpuMat d_data(1, 100, CV_32SC2);</div><div class="line">    // Thrust compatible begin and end iterators to channel 1 of this matrix</div><div class="line">    auto keyBegin = GpuMatBeginItr&lt;int&gt;(d_data, 1);</div><div class="line">    auto keyEnd = GpuMatEndItr&lt;int&gt;(d_data, 1);</div><div class="line">    // Thrust compatible begin and end iterators to channel 0 of this matrix</div><div class="line">    auto idxBegin = GpuMatBeginItr&lt;int&gt;(d_data, 0);</div><div class="line">    auto idxEnd = GpuMatEndItr&lt;int&gt;(d_data, 0);</div><div class="line">    // Fill the index channel with a sequence of numbers from 0 to 100</div><div class="line">    thrust::sequence(idxBegin, idxEnd);</div><div class="line">    // Fill the key channel with random numbers between 0 and 10.  A counting iterator is used here to give an integer value for each location as an input to prg::operator()</div><div class="line">    thrust::transform(thrust::make_counting_iterator(0), thrust::make_counting_iterator(d_data.cols), keyBegin, prg(0, 10));</div><div class="line">    // Sort the key channel and index channel such that the keys and indecies stay together</div><div class="line">    thrust::sort_by_key(keyBegin, keyEnd, idxBegin);</div><div class="line"></div><div class="line">    cv::Mat h_idx(d_data);</div><div class="line">  }</div></div><!-- fragment --> <h2>Copy values greater than 0 to a new gpu matrix while using streams </h2>
<p>In this example we're going to see how cv::cuda::Streams can be used with thrust. Unfortunately this specific example uses functions that must return results to the CPU so it isn't the optimal use of streams.</p>
<div class="fragment"><div class="line">  {</div><div class="line">    cv::cuda::GpuMat d_value(1, 100, CV_32F);</div><div class="line">    auto valueBegin = GpuMatBeginItr&lt;float&gt;(d_value);</div><div class="line">    auto valueEnd = GpuMatEndItr&lt;float&gt;(d_value);</div><div class="line">    cv::cuda::Stream stream;</div><div class="line">    //! [random_gen_stream]</div><div class="line">    // Same as the random generation code from before except now the transformation is being performed on a stream</div><div class="line">    thrust::transform(thrust::system::cuda::par.on(cv::cuda::StreamAccessor::getStream(stream)), thrust::make_counting_iterator(0), thrust::make_counting_iterator(d_value.cols), valueBegin, prg(-1, 1));</div><div class="line">    //! [random_gen_stream]</div><div class="line">    // Count the number of values we are going to copy</div><div class="line">    int count = thrust::count_if(thrust::system::cuda::par.on(cv::cuda::StreamAccessor::getStream(stream)), valueBegin, valueEnd, pred_greater&lt;float&gt;(0.0));</div><div class="line">    // Allocate a destination for copied values</div><div class="line">    cv::cuda::GpuMat d_valueGreater(1, count, CV_32F);</div><div class="line">    // Copy values that satisfy the predicate.</div><div class="line">    thrust::copy_if(thrust::system::cuda::par.on(cv::cuda::StreamAccessor::getStream(stream)), valueBegin, valueEnd, GpuMatBeginItr&lt;float&gt;(d_valueGreater), pred_greater&lt;float&gt;(0.0));</div><div class="line">    cv::Mat h_greater(d_valueGreater);</div><div class="line">  }</div></div><!-- fragment --><p>First we will populate a GPU mat with randomly generated data between -1 and 1 on a stream.</p>
<div class="fragment"><div class="line">    // Same as the random generation code from before except now the transformation is being performed on a stream</div><div class="line">    thrust::transform(thrust::system::cuda::par.on(cv::cuda::StreamAccessor::getStream(stream)), thrust::make_counting_iterator(0), thrust::make_counting_iterator(d_value.cols), valueBegin, prg(-1, 1));</div></div><!-- fragment --><p> Notice the use of thrust::system::cuda::par.on(...), this creates an execution policy for executing thrust code on a stream. There is a bug in the version of thrust distributed with the cuda toolkit, as of version 7.5 this has not been fixed. This bug causes code to not execute on streams. The bug can however be fixed by using the newest version of thrust from the git repository. (<a href="http://github.com/thrust/thrust.git">http://github.com/thrust/thrust.git</a>) Next we will determine how many values are greater than 0 by using thrust::count_if with the following predicate:</p>
<div class="fragment"><div class="line">template&lt;typename T&gt; struct pred_greater</div><div class="line">{</div><div class="line">  T value;</div><div class="line">  __host__ __device__ pred_greater(T value_) : value(value_){}</div><div class="line">  __host__ __device__ bool operator()(const T&amp; val) const</div><div class="line">  {</div><div class="line">    return val &gt; value;</div><div class="line">  }</div><div class="line">};</div></div><!-- fragment --><p> We will use those results to create an output buffer for storing the copied values, we will then use copy_if with the same predicate to populate the output buffer. Lastly we will download the values into a CPU mat for viewing. </p>
</div></div><!-- contents -->
<!-- HTML footer for doxygen 1.8.6-->
<!-- start footer part -->
<hr class="footer"/><address class="footer"><small>
Generated on Fri Apr 2 2021 11:36:35 for OpenCV by &#160;<a href="http://www.doxygen.org/index.html">
<img class="footer" src="../../doxygen.png" alt="doxygen"/>
</a> 1.8.13
</small></address>
<script type="text/javascript">
//<![CDATA[
addTutorialsButtons();
//]]>
</script>
</body>
</html>
